1. Variables Captured by Closures
In C#, a closure can be created with a lambda expression in the form args => body
that represents an unnamed (anonymous) delegate. A unique feature of
closures is that they may refer to variables defined outside their
lexical scope, such as local variables that were declared in a scope
that contains the closure.
The semantics of closures in C#
may not be intuitive to some programmers and it’s easy to make
mistakes. Unless you understand the semantics, you may find that
captured variables don’t behave as you expect, especially in parallel programs.
Problems occur when you reference a variable without considering its scope. Here’s an example.
for (int i = 0; i < 4; i++)
{
// WARNING: BUGGY CODE, i has unexpected value
Task.Factory.StartNew(() => Console.WriteLine(i));
}
Note:
Capturing a loop index variable in a closure is usually a bug. Be on the lookout for this very common coding error.
You might think that this
code sample would print the numbers 1, 2, 3, 4 in some arbitrary order,
but it can print other values, depending on how the threads happen to
run. For example, you might see 4, 4, 4, 4. The reason is that the
variable i is shared by all the closures created by the steps of the for loop. By the time the tasks start, the value of the single, shared variable i will probably be different from the value of i when the task was created. All the tasks are sharing the same variable.
The solution is to introduce an additional temporary variable in the appropriate scope.
for (int i = 0; i < 4; i++)
{
var tmp = i;
Task.Factory.StartNew(() => Console.WriteLine(tmp));
}
This version prints the
numbers 1, 2, 3, 4 in an arbitrary order, but each number will be
printed. The reason is that the variable tmp is declared within the block scope of the for loop’s body. This causes a new variable named tmp to be instantiated with each iteration of the for loop. (In contrast, all iterations of the for loop share a single instance of the variable i.)
This bug is one reason why you should use Parallel.For instead of coding a loop yourself. It’s also one of the most common mistakes made by programmers who are new to tasks.
2. Disposing a Resource Needed by a Task
When you create a task, don’t forget that you can’t call the Dispose method on the objects that the task needs to do its work. Careless use of the C# using keyword is a common way to make this mistake. Here’s an example.
Note:
Be careful not to dispose resources needed by a pending task.
Task<string> t;
using (var file = new StringReader("text"))
{
t = Task<string>.Factory.StartNew(() => file.ReadLine());
}
// WARNING: BUGGY CODE, file has been disposed
Console.WriteLine(t.Result);
The using keyword introduces an implicit try/finally block that invokes the Dispose method on the value of the variable when it exits its scope. The using keyword can’t be used with captured variables. Instead, you need to call the Dispose method when you know that the disposable object is no longer needed. In this example, Dispose can only be called after the task’s Result property is read.
3. Avoid Thread Abort
Terminating tasks with the Thread.Abort
method leaves the App Domain in a potentially unusable state. Also,
aborting a thread pool worker thread is never recommended.
Note:
Never attempt to cancel a task by calling the Abort method of the thread that is executing the task.